﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using Core.Domain;
using Core.Domain.Lexems;

namespace Core
{
    public class LexicalAnalyzer
    {
        private readonly List<string> m_sepparatorsForNumbers = new List<string> // separators for numbers (for error detection)
                                                                {
                                                                    " ",
                                                                    ",",
                                                                    ";",
                                                                    ")",
                                                                    // normal separators, can happen anywhere
                                                                    "\r",
                                                                    "\n",
                                                                    "\r\n",
                                                                    // new lines are like spaces
                                                                    "*",
                                                                    "-",
                                                                    "/",
                                                                    "+",
                                                                    // math operations
                                                                    ":",
                                                                    Environment.NewLine
                                                                }; // near labels

        private int m_column = 0;
        private char m_lookahead;
        private int m_row = 0;

        private int Row
        {
            get { return m_row; }
            set
            {
                m_row = value;
                m_column = 0;
            }
        }

        private char Lookahead
        {
            get { return m_lookahead; }
            set
            {
                m_lookahead = value;
                m_column++;
            }
        }

        /// <summary> Разбивает sourceCode на последовательность лексем lexems </summary>
        /// <param name="sourceCode"> исходный код программы </param>
        /// <param name="terminalWords"> список терминальных символов </param>
        /// <param name="lexems"> выходной список лексем </param>
        /// <param name="ids"> таблица идентификаторов </param>
        /// <param name="consts"> таблица констант </param>
        /// <param name="errors"> список ошибок</param>
        /// <returns> true if parsing was successful</returns>
        public bool Scan(String sourceCode, List<string> terminalWords, out List<Lexem> lexems, out Dictionary<string, int> ids, out Dictionary<string, int> consts, out List<CompilerError> errors)
        {
            int position = 0;
            int constNumber = 0;
            int nameNumber = 0;
            lexems = new List<Lexem>();
            ids = new Dictionary<string, int>();
            consts = new Dictionary<string, int>();
            errors = new List<CompilerError>();
            while (true)
            {
                if (position == sourceCode.Length)
                {
                    return errors.Count == 0;
                }
                Lookahead = sourceCode[position];
                int index;
                switch (Lookahead)
                {
                    case '{':
                        var commentEnd = sourceCode.IndexOf('}');
                        position = commentEnd < 0 ? sourceCode.Length : commentEnd + 1;
                        break;
                    case ' ':
                        position++;
                        break;
                    case '\r':
                    case '\n':
                        if (sourceCode[position + 1] == '\n')
                        {
                            position += 2;
                        }
                        else
                        {
                            position++;
                        }
                        Row++;
                        //lexems.Add(Lexem.NewLineLexem());
                        break;


                        #region Digit 0-9

                    case '0':
                    case '1':
                    case '2':
                    case '3':
                    case '4':
                    case '5':
                    case '6':
                    case '7':
                    case '8':
                    case '9':

                        #endregion


                        // Numbers: 812, 99.11. numbers can contain '.', but only 1. All decisions about is it a number or not will take C#'s Parse method
                        string number = ReadNumber(sourceCode, ref position);
                        Lexem constLexem = null;
                        int xInteger;
                        double xDouble;


                        #region look for this in constants, if there is a constant with such name - save it's index

                        if (!consts.ContainsKey(number))
                        {
                            index = constNumber;
                            constNumber++;
                            consts.Add(number, index);
                        }
                        else
                        {
                            index = consts[number];
                        }

                        #endregion


                        #region is the number integer or double?

                        if (Int32.TryParse(number, out xInteger))
                        {
                            constLexem = new ConstLexem<int>(xInteger, number, index, new PositionInText(Row, m_column));
                        }
                        else if (Double.TryParse(number, NumberStyles.Float, CultureInfo.InvariantCulture, out xDouble))
                        {
                            constLexem = new ConstLexem<double>(xDouble, number, index, new PositionInText(Row, m_column));
                        }

                        #endregion


                        if (constLexem == null) // if not integer, nor double - this is not a number at all
                        {
                            errors.Add(new CompilerError(errors.Count + 1, String.Format(Resources.InvalidConstantErrorMessage,  number), Row, m_column));
                        }
                        else
                        {
                            // if it is - add to constants
                            lexems.Add(constLexem);
                        }
                        break;


                        #region Letters

                    case 'a':
                    case 'b':
                    case 'c':
                    case 'd':
                    case 'e':
                    case 'f':
                    case 'g':
                    case 'h':
                    case 'i':
                    case 'j':
                    case 'k':
                    case 'l':
                    case 'm':
                    case 'n':
                    case 'o':
                    case 'p':
                    case 'q':
                    case 'r':
                    case 's':
                    case 't':
                    case 'u':
                    case 'v':
                    case 'w':
                    case 'x':
                    case 'y':
                    case 'z':
                    case 'A':
                    case 'B':
                    case 'C':
                    case 'D':
                    case 'E':
                    case 'F':
                    case 'G':
                    case 'H':
                    case 'I':
                    case 'J':
                    case 'K':
                    case 'L':
                    case 'M':
                    case 'N':
                    case 'O':
                    case 'P':
                    case 'Q':
                    case 'R':
                    case 'S':
                    case 'T':
                    case 'U':
                    case 'V':
                    case 'W':
                    case 'X':
                    case 'Y':
                    case 'Z':

                        #endregion


                    case '_':
                        // Names: identifiers and key words. Starts with letter, contains letters and digits
                        string name = ReadName(sourceCode, ref position);
                        Lexem nameLexem;
                        if (terminalWords.Contains(name.ToUpper()))
                        {
                            nameLexem = new TerminalLexem(name, terminalWords.IndexOf(name.ToUpper()), new PositionInText(Row, m_column));
                        }
                        else
                        {
                            #region look for this in ids and save index if there is one

                            if (!ids.ContainsKey(name.ToUpper()))
                            {
                                index = nameNumber;
                                nameNumber++;
                                ids.Add(name.ToUpper(), index);
                            }
                            else
                            {
                                index = ids[name.ToUpper()];
                            }

                            #endregion


                            nameLexem = new IdLexem(name, index, new PositionInText(Row, m_column));
                        }
                        lexems.Add(nameLexem);
                        break;


                        #region := <= >=

                    case ':':
                    case '>':
                    case '<':

                        #endregion


                        if (sourceCode[position + 1] == '=') // := >= <=
                        {
                            string op2 = Lookahead + "=";
                            position += 2;
                            var op2Lexem = new TerminalLexem(op2, terminalWords.IndexOf(op2), new PositionInText(Row, m_column));
                            lexems.Add(op2Lexem);
                            break;
                        }
                        goto case '+';


                        #region + - * / =  ( ) . ;

                    case '+':
                    case '-':
                    case '*':
                    case '/':
                    case '=':
                    case ',':
                    case '(':
                    case ')':
                    case '.':
                    case ';':

                        #endregion


                        string op = Lookahead.ToString();
                        position++;
                        var opLexem = new TerminalLexem(op, terminalWords.IndexOf(op), new PositionInText(Row, m_column));
                        lexems.Add(opLexem);
                        break;
                    default:
                        errors.Add(new CompilerError(errors.Count + 1, String.Format(Resources.UnexpectedSymbolErrorMessage,Lookahead), Row, m_column));
                        position++;
                        break;
                }
            }
        }

        /// <summary> Чтение числа </summary>
        /// <param name="source">Исходный код</param>
        /// <param name="position">Текущение положение указателя</param>
        /// <returns> Строка, содержащая прочитанное число</returns>
        private string ReadNumber(string source, ref int position)
        {
            var number = new StringBuilder();
            do
            {
                number.Append(Lookahead);
                position++;
                if (position < source.Length)
                {
                    Lookahead = source[position];
                }
                else
                {
                    break;
                }
            }
            while (!m_sepparatorsForNumbers.Contains(Lookahead.ToString()));
            return number.ToString();
        }

        /// <summary> Чтение идентификатора и ключевого слова </summary>
        /// <param name="source">Исходный код</param>
        /// <param name="position">Текущение положение указателя</param>
        /// <returns> Строка, содержащая считанную последовательность символов</returns>
        private string ReadName(string source, ref int position)
        {
            var name = new StringBuilder();
            do
            {
                name.Append(Lookahead);
                position++;
                if (position < source.Length)
                {
                    Lookahead = source[position];
                }
                else
                {
                    break;
                }
            }
            while (Char.IsDigit(Lookahead) || Char.IsLetter(Lookahead) || Lookahead == '_');
            return name.ToString();
        }
    }
}